package datart.server.config.handler;

import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.S3Object;
import com.cloud.common.oss.OssProperties;
import com.cloud.common.oss.service.OssTemplate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tomcat.util.http.fileupload.util.Streams;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.support.WebContentGenerator;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;

/**
 * @author 支绍昆
 * 2023/3/17
 * oss静态资源请求处理器
 **/
public class OssResourceHttpRequestHandler extends WebContentGenerator
        implements HttpRequestHandler, InitializingBean {

    private static final Log logger = LogFactory.getLog(OssResourceHttpRequestHandler.class);

    private final OssTemplate ossTemplate;

    private final OssProperties ossProperties;

    private boolean useLastModified = true;

    private final Map<String, MediaType> mediaTypes = new HashMap<>(4);

    @Nullable
    private ContentNegotiationManager contentNegotiationManager;

    public OssResourceHttpRequestHandler(OssTemplate ossTemplate, OssProperties ossProperties) {
        super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
        this.ossTemplate = ossTemplate;
        this.ossProperties = ossProperties;
    }

    public void setUseLastModified(boolean useLastModified) {
        this.useLastModified = useLastModified;
    }

    public boolean isUseLastModified() {
        return this.useLastModified;
    }

    public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) {
        this.contentNegotiationManager = contentNegotiationManager;
    }

    @Nullable
    public ContentNegotiationManager getContentNegotiationManager() {
        return this.contentNegotiationManager;
    }

    public void setMediaTypes(Map<String, MediaType> mediaTypes) {
        mediaTypes.forEach((ext, mediaType) ->
                this.mediaTypes.put(ext.toLowerCase(Locale.ENGLISH), mediaType));
    }

    public Map<String, MediaType> getMediaTypes() {
        return this.mediaTypes;
    }

    @Override
    public void afterPropertiesSet() {
        ContentNegotiationManager manager = getContentNegotiationManager();
        if (manager != null) {
            setMediaTypes(manager.getMediaTypeMappings());
        }
    }

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        S3Object objectInfo = null;
        try {
            objectInfo = ossTemplate.getObjectInfo(ossProperties.getBucketName(), parsePath(request));

            if (Objects.isNull(objectInfo)) {
                logger.debug("Resource not found");
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }

            if (HttpMethod.OPTIONS.matches(request.getMethod())) {
                response.setHeader("Allow", getAllowHeader());
                return;
            }

            // Supported methods and required session
            checkRequest(request);

            // Header phase
            if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(objectInfo.getObjectMetadata().getLastModified().getTime())) {
                logger.trace("Resource not modified");
                return;
            }

            // Apply cache settings, if any
            prepareResponse(response);

            // Check the media type for the resource
            setMediaType(request, response, objectInfo);
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");

            Streams.copy(objectInfo.getObjectContent(), response.getOutputStream(), true);

        } catch (AmazonS3Exception e) {
            response.setStatus(e.getStatusCode());
            response.sendError(e.getStatusCode());
        } catch (Exception e) {
            logger.error("oss get file error ", e);
            throw new RuntimeException(e);
        } finally {
            //注意一定要释放资源
            if (Objects.nonNull(objectInfo)) {
                objectInfo.close();
            }
        }
    }

    private String parsePath(HttpServletRequest request) {
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        if (path == null) {
            throw new IllegalStateException("Required request attribute '" +
                    HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
        }

        path = processPath(path);
        if (!StringUtils.hasText(path) || isInvalidPath(path)) {
            throw new IllegalArgumentException("resource path is invalid : " + path);
        }
        if (isInvalidEncodedPath(path)) {
            throw new IllegalArgumentException("resource path is invalid encoded : " + path);
        }
        return path;
    }

    protected S3Object getObjectInfo(HttpServletRequest request) throws Exception {
        //http://localhost:9001/res/images/dashboard/backgroud2455.jpg >> images/dashboard/backgroud2455.jpg
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        if (path == null) {
            throw new IllegalStateException("Required request attribute '" +
                    HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
        }

        path = processPath(path);
        if (!StringUtils.hasText(path) || isInvalidPath(path)) {
            return null;
        }
        if (isInvalidEncodedPath(path)) {
            return null;
        }

        return ossTemplate.getObjectInfo(ossProperties.getBucketName(), path);
    }

    // -------------------------------------- private ------------------------------------------------

    private String processPath(String path) {
        path = StringUtils.replace(path, "\\", "/");
        path = cleanDuplicateSlashes(path);
        return cleanLeadingSlash(path);
    }

    private String cleanDuplicateSlashes(String path) {
        StringBuilder sb = null;
        char prev = 0;
        for (int i = 0; i < path.length(); i++) {
            char curr = path.charAt(i);
            try {
                if ((curr == '/') && (prev == '/')) {
                    if (sb == null) {
                        sb = new StringBuilder(path.substring(0, i));
                    }
                    continue;
                }
                if (sb != null) {
                    sb.append(path.charAt(i));
                }
            } finally {
                prev = curr;
            }
        }
        return sb != null ? sb.toString() : path;
    }

    private String cleanLeadingSlash(String path) {
        boolean slash = false;
        for (int i = 0; i < path.length(); i++) {
            if (path.charAt(i) == '/') {
                slash = true;
            } else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
                if (i == 0 || (i == 1 && slash)) {
                    return path;
                }
                return (slash ? "/" + path.substring(i) : path.substring(i));
            }
        }
        return (slash ? "/" : "");
    }

    private boolean isInvalidPath(String path) {
        if (path.contains("WEB-INF") || path.contains("META-INF")) {
            if (logger.isWarnEnabled()) {
                logger.warn(LogFormatUtils.formatValue(
                        "Path with \"WEB-INF\" or \"META-INF\": [" + path + "]", -1, true));
            }
            return true;
        }
        if (path.contains(":/")) {
            String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
            if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
                if (logger.isWarnEnabled()) {
                    logger.warn(LogFormatUtils.formatValue(
                            "Path represents URL or has \"url:\" prefix: [" + path + "]", -1, true));
                }
                return true;
            }
        }
        if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {
            if (logger.isWarnEnabled()) {
                logger.warn(LogFormatUtils.formatValue(
                        "Path contains \"../\" after call to StringUtils#cleanPath: [" + path + "]", -1, true));
            }
            return true;
        }
        return false;
    }

    private boolean isInvalidEncodedPath(String path) {
        if (path.contains("%")) {
            try {
                // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
                String decodedPath = URLDecoder.decode(path, "UTF-8");
                if (isInvalidPath(decodedPath)) {
                    return true;
                }
                decodedPath = processPath(decodedPath);
                if (isInvalidPath(decodedPath)) {
                    return true;
                }
            } catch (IllegalArgumentException ex) {
                // May not be possible to decode...
            } catch (UnsupportedEncodingException ex) {
                // Should never happen...
            }
        }
        return false;
    }

    private void setMediaType(HttpServletRequest request, HttpServletResponse response, S3Object objectInfo) {
        if (Objects.nonNull(objectInfo.getObjectMetadata().getContentType())) {
            response.setContentType(objectInfo.getObjectMetadata().getContentType());
            return;
        }

        MediaType result = null;
        String fileName = getName(objectInfo.getKey());
        String mimeType = request.getServletContext().getMimeType(fileName);
        if (StringUtils.hasText(mimeType)) {
            response.setContentType(mimeType);
            result = MediaType.parseMediaType(mimeType);
        }
        if (result == null || MediaType.APPLICATION_OCTET_STREAM.equals(result)) {
            MediaType mediaType = null;
            String ext = StringUtils.getFilenameExtension(fileName);
            if (ext != null) {
                mediaType = this.mediaTypes.get(ext.toLowerCase(Locale.ENGLISH));
            }
            if (mediaType == null) {
                List<MediaType> mediaTypes = MediaTypeFactory.getMediaTypes(fileName);
                if (!CollectionUtils.isEmpty(mediaTypes)) {
                    mediaType = mediaTypes.get(0);
                }
            }
            if (mediaType != null) {
                result = mediaType;
            }
        }
        if (Objects.nonNull(result)) {
            response.setContentType(result.toString());
        }
    }

    private String getName(String path) {
        int index = path.lastIndexOf('/');
        if (index > 0) return path.substring(index + 1);
        return path;
    }

}